import { ErrDuplicated } from './error'
import { MsgDoOp, OpAction, OpCancel, OpCompensate, OpRollback, OpTry } from './constants'
import { Barrier } from './dao'
import { getLogger } from './logger'
import { ClientSession } from "mongoose";

// BarrierBusiFunc type for busi func
export type BarrierBusiFunc = (session: ClientSession) => Promise<void>

const OP_MAP: Record<string, string> = {
  [OpCancel]: OpTry, // tcc
  [OpCompensate]: OpAction, // saga
  [OpRollback]: OpAction, // workflow
}

export class BranchBarrier {
  transType: string
  gid: string
  branchId: string
  op: string
  barrierId = 0
  dbType: string
  barrierTableName: string

  constructor(transType: string, gid: string, branchId: string, op: string) {
    this.transType = transType
    this.gid = gid
    this.branchId = branchId
    this.op = op
    if (this.transType === '' || this.gid === '' || this.branchId === '' || this.op === '') {
      throw new Error(`invalid trans info ${JSON.stringify(this)}`)
    }
  }

  toString(): string {
    return `transInfo: ${this.transType} ${this.gid} ${this.branchId} ${this.op}`
  }

  async call(session: ClientSession, busiCall: BarrierBusiFunc): Promise<void> {
    const bid = this.newBarrierId()
    const originOp = OP_MAP[this.op]
    try {
      session.startTransaction();
      const result1 = await Barrier.findOneAndUpdate(
        {
          transType: this.transType,
          gid: this.gid,
          branchId: this.branchId,
          op: originOp,
          barrierId: bid,
          reason: this.op,
        }, {},
        { session, rawResult: true, upsert: true }
      );
      if (result1.ok != 1 ){
        throw result1;
      }
      const originCreated = !result1?.lastErrorObject?.updatedExisting;

      const result2 = await Barrier.findOneAndUpdate(
        {
          transType: this.transType,
          gid: this.gid,
          branchId: this.branchId,
          op: this.op,
          barrierId: bid,
          reason: this.op,
        },{},
        { session, rawResult: true, upsert: true }
      );
      if (result2.ok != 1 ){
        throw result2;
      }
      const currentCreated = !result2?.lastErrorObject?.updatedExisting;

      if (this.op === MsgDoOp && !currentCreated) {
        // for msg's DoAndSubmit, repeated insert should be rejected.
        throw ErrDuplicated
      }

      if (
        ((this.op === OpCancel || this.op === OpCompensate || this.op === OpRollback) && originCreated) || // null compensate
        !currentCreated // repeated request or dangled request
      ) {
        await session.commitTransaction();
        return;
      }

      await busiCall(session)

      await session.commitTransaction();
    } catch (error: unknown) {
      getLogger().error(`[dtmcli] barrier call error: ${error}`)
      await session.abortTransaction();
      throw error;
    } finally {
      await session.endSession();
    }
  }

  async callWithDB(busiCall: BarrierBusiFunc): Promise<void> {
    const session = await Barrier.startSession();
    await this.call(session, busiCall)
  }

  private newBarrierId(): string {
    this.barrierId++
    if (this.barrierId >= 10) {
      return String(this.barrierId)
    }
    return `0${this.barrierId}`
  }
}
